package cmd

import (
	"fmt"
	"io"
	"k8s.io/kubernetes/cmd/kubeadm/app/constants"

	ckeapi "cke.io/ckectl/app/apis"
	ckeapiv1beta2 "cke.io/ckectl/app/apis/v1beta2"
	"cke.io/ckectl/app/cmd/options"
	common "cke.io/ckectl/app/cmd/phases"
	phases "cke.io/ckectl/app/cmd/phases/up"
	kubeadmutil "cke.io/ckectl/app/util"
	"github.com/pkg/errors"
	"github.com/spf13/cobra"
	flag "github.com/spf13/pflag"
	kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme"
	kubeadmapiv1beta2 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta2"
	kubeoptions "k8s.io/kubernetes/cmd/kubeadm/app/cmd/options"
	"k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases/workflow"
	configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config"
)

// NewCmdInit returns "kubeadm init" command.
// NB. initOptions is exposed as parameter for allowing unit testing of
//     the newInitOptions method, that implements all the command options validation logic
func NewCmdPrepare(out io.Writer, prepareOptions *prepareOptions) *cobra.Command {
	if prepareOptions == nil {
		prepareOptions = newPrepareOptions()
	}
	prepareRunner := workflow.NewRunner()

	cmd := &cobra.Command{
		Use:   "prepare",
		Short: "Run this command in order to prepare files for the Kubernetes Deploy",
		Run: func(cmd *cobra.Command, args []string) {
			c, err := prepareRunner.InitData(args)
			kubeadmutil.CheckErr(err)

			data := c.(*prepareData)
			// fmt.Printf("[prepare] Using Kubernetes version: %s\n", data.cfg.KubernetesVersion)

			// flase done file when start up
			common.FlashAndSetDoneFile("/dev/null")
			err = prepareRunner.Run(args)
			kubeadmutil.CheckErr(err)

			err = showPrepareResult(prepareRunner, data, out)
			kubeadmutil.CheckErr(err)
		},
		Args: cobra.NoArgs,
	}

	// adds flags to the init command
	// init command local flags could be eventually inherited by the sub-commands automatically generated for phases
	AddPrepareConfigFlags(cmd.Flags(), prepareOptions.externalClusterCfg, prepareOptions.externalCkeCfg)
	AddPrepareOtherFlags(cmd.Flags(), prepareOptions)

	// initialize the workflow runner with the list of phases
	prepareRunner.AppendPhase(phases.NewPreparePhase())
	SetPhasesFlags(prepareRunner.Phases, GetFlagSetNames(cmd.Flags())...)

	// sets the data builder function, that will be used by the runner
	// both when running the entire workflow or single phases
	prepareRunner.SetDataInitializer(func(cmd *cobra.Command, args []string) (workflow.RunData, error) {
		return newPrepareData(cmd, args, prepareOptions, out)
	})

	// binds the Runner to kubeadm init command by altering
	// command help, adding --skip-phases flag and by adding phases subcommands
	prepareRunner.BindToCommand(cmd)

	return cmd
}

type prepareData struct {
	cfg             *ckeapi.CkeConfiguration
	outputWriter    io.Writer
}

// initOptions defines all the init options exposed via flags by kubeadm init.
// Please note that this structure includes the public kubeadm config API, but only a subset of the options
// supported by this api will be exposed as a flag.
type prepareOptions struct {
	cfgPath                 string
	externalCkeCfg          *ckeapiv1beta2.CkeConfiguration
	externalClusterCfg      *kubeadmapiv1beta2.ClusterConfiguration
}

// AddClusterConfigFlags adds cluster flags bound to the config to the specified flagset
func AddPrepareConfigFlags(flagSet *flag.FlagSet, cluster *kubeadmapiv1beta2.ClusterConfiguration, cke *ckeapiv1beta2.CkeConfiguration) {
	flagSet.StringVar(
		&cke.Ansible.LocalIp, options.LocalIp, cke.Ansible.LocalIp,
		"Use alternative range of IP address for service VIPs.",
	)
	flagSet.StringVar(
		&cke.Ansible.LocalHostName, options.LocalHostName, cke.Ansible.LocalHostName,
		"Specify range of IP addresses for the pod network. If set, the control plane will automatically allocate CIDRs for every node.",
	)
	flagSet.StringVar(
		&cke.Ansible.AnsibleImageName, options.AnsibleImageName, cke.Ansible.AnsibleImageName,
		`Use alternative domain for services, e.g. "myorg.internal".`,
	)
	flagSet.StringVar(
		&cke.Ansible.NodesConfig, options.NodeConfig, cke.Ansible.NodesConfig,
		"Specify range of IP addresses for the pod network. If set, the control plane will automatically allocate CIDRs for every node.",
	)
	flagSet.StringVar(
		&cke.Ansible.AllConfig, options.ALLConfig, cke.Ansible.AllConfig,
		`Use alternative domain for services, e.g. "myorg.internal".`,
	)
	flagSet.StringVar(
		&cke.Ansible.ClusterID, options.ClusterID, cke.Ansible.ClusterID,
		"Specify range of IP addresses for the pod network. If set, the control plane will automatically allocate CIDRs for every node.",
	)
	flagSet.StringVar(
		&cke.Ansible.CkeadmWaitAddress, options.CkeadmWaitAddress, cke.Ansible.CkeadmWaitAddress,
		`Use alternative domain for services, e.g. "myorg.internal".`,
	)
	flagSet.StringVar(
		&cluster.ImageRepository, kubeoptions.ImageRepository, cluster.ImageRepository,
		`Use alternative domain for services, e.g. "myorg.internal".`,
	)
	flagSet.StringSliceVar(
		&cke.InsecureRegistry, options.InsecureRegistry, cke.InsecureRegistry,
		`Use alternative domain for services, e.g. "myorg.internal".`,
	)
	flagSet.StringSliceVar(
		&cke.Ansible.Mounts, options.AnsibleMounts, cke.Ansible.Mounts,
		`Use alternative domain for services, e.g. "myorg.internal".`,
	)
	flagSet.StringSliceVar(
		&cke.Ansible.Domains, options.AllKnowDomains, cke.Ansible.Domains,
		`Use alternative domain for services, e.g. "myorg.internal".`,
	)
	flagSet.StringSliceVar(
		&cke.Ansible.AllowKnowHosts, options.AllowKnowHosts, cke.Ansible.AllowKnowHosts,
		`Use alternative domain for services, e.g. "myorg.internal".`,
	)
	flagSet.StringSliceVar(
		&cke.Ansible.ExternalArguments, options.ExternalArguments, cke.Ansible.ExternalArguments,
		`Use alternative domain for services, e.g. "myorg.internal".`,
	)
	flagSet.StringVar(
		&cke.Ansible.SshSecretKey, options.SshSecretKey, cke.Ansible.SshSecretKey,
		`Use alternative domain for services, e.g. "myorg.internal".`,
	)
	flagSet.StringSliceVar(
		&cke.Ansible.DockerPreconfiged, options.DockerPreconfiged, cke.Ansible.DockerPreconfiged,
		`Use alternative domain for services, e.g. "myorg.internal".`,
	)
	flagSet.BoolVar(
		&cluster.UseHyperKubeImage, options.UseHyperKubeImage, cluster.UseHyperKubeImage,
		"Specify range of IP addresses for the pod network. If set, the control plane will automatically allocate CIDRs for every node.",
	)
	kubeoptions.AddKubernetesVersionFlag(flagSet, &cluster.KubernetesVersion)
}

// AddInitOtherFlags adds init flags that are not bound to a configuration file to the given flagset
// Note: All flags that are not bound to the cfg object should be allowed in cmd/kubeadm/app/apis/kubeadm/validation/validation.go
func AddPrepareOtherFlags(flagSet *flag.FlagSet, prepareOptions *prepareOptions) {
	kubeoptions.AddConfigFlag(flagSet, &prepareOptions.cfgPath)
}

// newInitData returns a new initData struct to be used for the execution of the kubeadm init workflow.
// This func takes care of validating initOptions passed to the command, and then it converts
// options into the internal InitConfiguration type that is used as input all the phases in the kubeadm init workflow
func newPrepareData(cmd *cobra.Command, args []string, options *prepareOptions, out io.Writer) (*prepareData, error) {
	// Re-apply defaults to the public kubeadm API (this will set only values not exposed/not set as a flags)
	kubeadmscheme.Scheme.Default(options.externalCkeCfg)
	kubeadmscheme.Scheme.Default(options.externalClusterCfg)

	externalClusterCfg := &options.externalCkeCfg.InitConfiguration
	externalClusterCfg.NodeRegistration.CRISocket = constants.DefaultDockerCRISocket // defaults use docker to create nodes

	// Either use the config file if specified, or convert public kubeadm API to the internal InitConfiguration
	// and validates InitConfiguration
	initcfg, err := configutil.LoadOrDefaultInitConfiguration(options.cfgPath, externalClusterCfg, options.externalClusterCfg)
	if err != nil {
		return nil, err
	}

	var ckecfg = ckeapiv1beta2.LoadOrDefaultCkeConfiguration(initcfg, options.externalCkeCfg)
	return &prepareData{
		cfg:          &ckecfg,
		outputWriter: out,
	}, nil
}

// newInitOptions returns a struct ready for being used for creating cmd init flags.
func newPrepareOptions() *prepareOptions {
	// initialize the public kubeadm config API by applying defaults
	externalInitCfg := &kubeadmapiv1beta2.InitConfiguration{}

	externalClusterCfg := &kubeadmapiv1beta2.ClusterConfiguration{UseHyperKubeImage: true, KubernetesVersion: "v1.16.2"}

	externalCkeCfg := &ckeapiv1beta2.CkeConfiguration{InitConfiguration: *externalInitCfg}

	return &prepareOptions{
		externalCkeCfg:     externalCkeCfg,
		externalClusterCfg: externalClusterCfg,
	}
}

// showPrepareResult prints the result after all the phases in prepare have finished
func showPrepareResult(runner *workflow.Runner, i *prepareData, out io.Writer) error {
	var pareResult = fmt.Sprintf("%d phases done:", len(runner.Phases))

	// showPrepareResult prints the result after all the phases in prepare have finished
	for _, phase := range runner.Phases {
		pareResult = fmt.Sprintf("%s %s,", pareResult, phase.Name)
	}

	if _, err := fmt.Fprintln(out, pareResult[:len(pareResult)-1]); err != nil {
		return errors.Wrap(err, "failed to print prepare result")
	}

	return nil
}

func (d *prepareData) Cfg() *ckeapi.CkeConfiguration {
	return d.cfg
}

func (d *prepareData) OutputWriter() io.Writer {
	return d.outputWriter
}

func GetFlagSetNames(set *flag.FlagSet) (names []string) {
	set.VisitAll(func(flag2 *flag.Flag) {
		names = append(names, flag2.Name)
	})
	return
}

func SetPhasesFlags(phases []workflow.Phase, options ...string) {
	for item := range phases {
		phases[item].InheritFlags = options
		SetPhasesFlags(phases[item].Phases, options...)
	}
}